⚡⏰

🔬 触发器和事件 · 上机实验

💻 在自己电脑的 MySQL 上完成 ⏱️ 预计 45 分钟 📦 配套讲义 第16章
🎯 这节课你要亲手做到
  • 自己写出 BEFORE / AFTER 触发器,并跑出预期结果
  • 会用 NEW / OLD 拿到新旧值,做校验和记日志
  • 创建一个会"自己定时跑"的事件,亲眼看到它执行
  • 遇到报错能对照"三个坑"自己排查

这是一节动手课:每个任务都先自己写、自己跑,跑不出来再看提示,最后对参考答案。先看一眼速查区,然后从环境准备开始。👇

📌 动手前 · 30 秒速查

触发器模板(照着填空)
DELIMITER $$
CREATE TRIGGER 触发器名
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON 表名
FOR EACH ROW
BEGIN
    -- 触发时执行的 SQL
END $$
DELIMITER ;

最容易踩的三个坑

坑① NEW/OLD 用错
插入用 NEW、删除用 OLD、修改两个都行。
坑② BEFORE/AFTER 选错
想改数据用 BEFORE、想记日志用 AFTER
坑③ 事件不跑
调度器没开:SET GLOBAL event_scheduler = ON;
0

环境准备:建好练习用的表

把下面整段复制到 MySQL 里一次性运行,所有任务都用这套数据

🛠️ 第一步:建库、建表、灌数据

直接整段复制到 Navicat / 命令行 / Workbench 运行即可。已自带一个学生(s3)成绩为空,后面任务会用到。

SQL · 一键运行
-- 1. 建库并使用
CREATE DATABASE IF NOT EXISTS db_trigger_lab DEFAULT CHARSET utf8mb4;
USE db_trigger_lab;

-- 2. 重新建表(先删后建,方便反复练习)
DROP TABLE IF EXISTS sc;
DROP TABLE IF EXISTS s;
DROP TABLE IF EXISTS c;

CREATE TABLE s (          -- 学生表
    sno  CHAR(5)     PRIMARY KEY,   -- 学号
    sn   VARCHAR(10) NOT NULL,      -- 姓名
    sex  CHAR(2),                    -- 性别
    maj  VARCHAR(30)                -- 专业
);

CREATE TABLE c (          -- 课程表
    cno  CHAR(5)     PRIMARY KEY,   -- 课程号
    cn   VARCHAR(30) NOT NULL       -- 课程名
);

CREATE TABLE sc (         -- 选课成绩表
    sno   CHAR(5),
    cno   CHAR(5),
    score DECIMAL(5,1),               -- 成绩(允许为空)
    PRIMARY KEY (sno, cno)
);

-- 3. 灌入测试数据
INSERT INTO s VALUES
('s1','张三','男','信息管理'),
('s2','李四','女','信息管理'),
('s3','王五','男','计算机');

INSERT INTO c VALUES
('c1','数据库原理'),
('c2','高等数学'),
('c3','Python程序设计');

INSERT INTO sc VALUES
('s1','c1',88.0),
('s1','c2',76.5),
('s2','c1',92.0);

🛠️ 第二步:打开事件调度器(拓展任务要用)

SQL
SET GLOBAL event_scheduler = ON;
SHOW VARIABLES LIKE 'event_scheduler';   -- 确认显示 ON
✅ 准备完成的标志

左侧能看到 scsc 三张表;执行 SELECT * FROM s; 能看到 3 个学生;调度器显示 ON。三项都对,就可以开始了。

1

实验一 · 基础任务

人人都要会 —— 几乎是讲义例子的重做,先自己写再看答案
任务 1 ⭐ 基础 BEFORE INSERT

sc 表上创建一个触发器 trg_score_zero插入成绩时,如果 score 是空的(NULL),自动改成 0 再存进去。

💡 卡住了?看这一条提示

① 要"改将要写入的值" → 用 BEFORE;是插入 → INSERT

② 在 BEGIN…END 里写:IF NEW.score IS NULL THEN SET NEW.score = 0; END IF;

③ 别忘了首尾的 DELIMITER $$DELIMITER ;

🎯 怎么验证
SQL · 自测
INSERT INTO sc VALUES ('s3', 'c2', NULL);   -- 故意插一个空成绩
SELECT * FROM sc WHERE sno='s3' AND cno='c2';
▶ 你应该看到(score 变成了 0,不是 NULL)sno cno score s3 c2 0.0
参考答案
SQL
DELIMITER $$

CREATE TRIGGER trg_score_zero
BEFORE INSERT ON sc
FOR EACH ROW
BEGIN
    IF NEW.score IS NULL THEN
        SET NEW.score = 0;
    END IF;
END $$

DELIMITER ;

关键就一句 SET NEW.score = 0; —— 在 BEFORE 里改 NEW,真的会改掉写进库的值。

任务 2 ⭐ 基础 AFTER UPDATE · 记日志

学生改专业时,自动记一条日志:学号、旧专业、新专业、修改时间。先建好日志表,再写触发器 trg_log_maj

📋 先建好这张日志表(复制运行)
SQL
CREATE TABLE maj_log (
    sno    CHAR(5),
    old_maj VARCHAR(30),
    new_maj VARCHAR(30),
    utime  DATETIME
);
💡 卡住了?看这一条提示

① 是"记录已发生的事" → 用 AFTER;是修改 → UPDATE

② 旧专业 = OLD.maj,新专业 = NEW.maj,时间 = NOW(),一句 INSERT INTO maj_log VALUES(...) 搞定。

🎯 怎么验证
SQL · 自测
UPDATE s SET maj = '软件工程' WHERE sno = 's1';
SELECT * FROM maj_log;
▶ 你应该看到一条日志(时间是你运行的当下)sno old_maj new_maj utime s1 信息管理 软件工程 2025-06-15 10:30:00
参考答案
SQL
DELIMITER $$

CREATE TRIGGER trg_log_maj
AFTER UPDATE ON s
FOR EACH ROW
BEGIN
    INSERT INTO maj_log VALUES
    (OLD.sno, OLD.maj, NEW.maj, NOW());
END $$

DELIMITER ;

这是 UPDATE 触发器唯一会同时用上 OLD 和 NEW 的经典套路:旧值 + 新值 + 时间,塞进日志表。

2

实验二 · 提高任务

综合一点,提示更少 —— 先在纸上想清楚框架再动手
任务 3 ⭐⭐ 提高 BEFORE DELETE · 级联

删除学生表 s 中的某个学生时,自动把他在 sc 表里的所有选课记录也删掉。创建触发器 trg_cascade_del

💡 提示

s 之前先去删 sc → 用 BEFORE DELETE。DELETE 触发器只有 OLD,要删的学生学号就是 OLD.sno,写一句 DELETE FROM sc WHERE sno = OLD.sno; 即可。

🎯 怎么验证
SQL · 自测
SELECT COUNT(*) FROM sc WHERE sno='s1';   -- 删之前:s1 有几条选课
DELETE FROM s WHERE sno='s1';          -- 删掉学生 s1
SELECT COUNT(*) FROM sc WHERE sno='s1';   -- 删之后:应该是 0
▶ 删之前 → 删之后删除前 COUNT(*) = 2 删除后 COUNT(*) = 0 ← s1 的选课记录被自动清空了
参考答案
SQL
DELIMITER $$

CREATE TRIGGER trg_cascade_del
BEFORE DELETE ON s
FOR EACH ROW
BEGIN
    DELETE FROM sc WHERE sno = OLD.sno;
END $$

DELIMITER ;

记住:DELETE 触发器里只有 OLD,没有 NEW,因为记录马上就要没了,只剩"以前"。

任务 4 ⭐⭐ 提高 事件 · EVERY(当堂看效果)

创建一个每 20 秒自动跑一次的事件 evt_heartbeat,每次往一张"心跳表"插入一条带当前时间的记录。等一分钟后查询,你会看到记录在自己增加 —— 这就是事件在自动工作。

📋 先建好心跳表
SQL
CREATE TABLE heartbeat (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    beat_time DATETIME
);
💡 提示

① "每隔一段时间" → 用 EVERY,写 ON SCHEDULE EVERY 20 SECOND

② 只有一条 SQL,可以直接 DO INSERT INTO heartbeat(beat_time) VALUES (NOW());,不用 BEGIN…END。

🎯 怎么验证
SQL · 等一分钟后多查几次
SELECT * FROM heartbeat ORDER BY id;
▶ 每隔 20 秒多一行(时间间隔约 20 秒)id beat_time 1 2025-06-15 10:40:00 2 2025-06-15 10:40:20 3 2025-06-15 10:40:40 ← 行数会一直涨
⚠️ 看完效果记得"关掉"它,别让它一直插

练完执行 DROP EVENT IF EXISTS evt_heartbeat; 把事件删掉。另外:这里用 20 秒只是为了课堂能当场看到效果;真实项目里清理/备份类事件一般是 EVERY 1 DAY 这种长周期。

参考答案
SQL
-- 调度器要先开(环境准备里已开)
CREATE EVENT evt_heartbeat
ON SCHEDULE EVERY 20 SECOND
DO
    INSERT INTO heartbeat(beat_time) VALUES (NOW());

如果查出来一直只有 0 行,99% 是调度器没开 —— 回到环境准备执行 SET GLOBAL event_scheduler = ON;

3

实验三 · 拓展任务

学有余力再做 —— 把"校验 + 记日志"两个触发器配合起来
任务 5 ⭐⭐⭐ 拓展 订单系统小综合

给一个迷你订单系统加两个触发器,让它"自己照看自己"。

📋 先建好订单表和状态日志表
SQL
CREATE TABLE orders (
    oid    CHAR(6) PRIMARY KEY,
    amount DECIMAL(10,2),
    status VARCHAR(10)        -- 订单状态,可能为空
);

CREATE TABLE order_log (
    oid        CHAR(6),
    old_status VARCHAR(10),
    new_status VARCHAR(10),
    ctime      DATETIME
);

需求 A:插入订单时,如果 status 为空,自动设为 '待付款'
需求 B:修改订单状态时,把"订单号 / 旧状态 / 新状态 / 时间"记到 order_log仅在状态真的变了才记

💡 提示

需求 A → BEFORE INSERT,套任务 1 的写法,把判断换成 NEW.status IS NULL

需求 B → AFTER UPDATE,套任务 2 的写法,但外面包一层 IF OLD.status != NEW.status THEN … END IF; 避免没改也记一条。

🎯 怎么验证
SQL · 自测
-- 验证 A:插一条不带 status 的订单
INSERT INTO orders(oid, amount) VALUES ('o1001', 99.00);
SELECT * FROM orders;          -- status 应是 '待付款'

-- 验证 B:改状态,看日志
UPDATE orders SET status='已付款' WHERE oid='o1001';
SELECT * FROM order_log;       -- 应有一条 待付款→已付款
▶ orders 表 + order_log 表orders: oid=o1001 amount=99.00 status=待付款 order_log: o1001 待付款 已付款 2025-06-15 11:00:00
参考答案

需求 A · BEFORE INSERT

SQL
DELIMITER $$
CREATE TRIGGER trg_order_default
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
    IF NEW.status IS NULL THEN
        SET NEW.status = '待付款';
    END IF;
END $$
DELIMITER ;

需求 B · AFTER UPDATE(只在状态变了才记)

SQL
DELIMITER $$
CREATE TRIGGER trg_order_log
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
    IF OLD.status != NEW.status THEN
        INSERT INTO order_log VALUES
        (OLD.oid, OLD.status, NEW.status, NOW());
    END IF;
END $$
DELIMITER ;

两个触发器各管一段:A 负责"进门时给默认值",B 负责"改动留痕"。这正是讲义里"校验用 BEFORE、记日志用 AFTER"的组合实战。

🤔 想一想(不用写代码)

需求 B 如果去掉 IF OLD.status != NEW.status 这层判断,会发生什么?提示:哪怕你执行一个"啥也没改"的 UPDATE,日志表也会……?

完成检查

勾一勾,看看自己走到哪了

完成进度:0 / 6

🎉 做完前 4 个任务,你就已经掌握本章全部核心了

回顾一下你今天真正动手做出来的:BEFORE/AFTER 触发器、NEW/OLD 取值、级联删除、会自动跑的事件。这四样,就是触发器和事件的全部要点。